package com.ipan.kits.io;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ipan.kits.base.Platforms;
import com.ipan.kits.base.annotation.NotNull;
import com.ipan.kits.base.annotation.Nullable;
import com.ipan.kits.collection.ArrayUtil;
import com.ipan.kits.net.UrlUtil;
import com.ipan.kits.text.CharPool;
import com.ipan.kits.text.Charsets;
import com.ipan.kits.text.MoreStringUtil;
import com.ipan.kits.text.StrPool;

/**
 * 关于文件的工具集.
 * 
 * 主要是调用JDK自带的Files工具类，少量代码调用Guava Files。 固定encoding为UTF8.
 * 
 * 1.文件读写
 * 
 * 2.文件及目录操作
 */
public class FileUtil {
	
	private static Logger log = LoggerFactory.getLogger(FileUtil.class);
	
	private static final int BUFFER_SIZE = 1024 * 3;
	/**
	 * .java文件扩展名
	 */
	public static final String EXT_JAVA = ".java";
	/**
	 * .class文件扩展名
	 */
	public static final String EXT_CLASS = ".class";
	/**
	 * .jar文件扩展名
	 */
	public static final String EXT_JAR = ".jar";

	// fileVisitor for file deletion on Files.walkFileTree
	private static FileVisitor<Path> deleteFileVisitor = new SimpleFileVisitor<Path>() {

		@Override
		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
			Files.delete(file);
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
			Files.delete(dir);
			return FileVisitResult.CONTINUE;
		}
	};

	//////// 文件读写//////

	/**
	 * 读取文件到byte[].
	 * 
	 * @see {@link Files#readAllBytes}
	 */
	public static byte[] toByteArray(final File file) throws IOException {
		return Files.readAllBytes(file.toPath());
	}

	/**
	 * 读取文件到String.
	 */
	public static String toString(final File file) throws IOException {
		return com.google.common.io.Files.toString(file, Charsets.UTF_8);
	}
	/**
	 * 读取文件到String.
	 */
	public static String toString(final File file, Charset charset) throws IOException {
		return com.google.common.io.Files.toString(file, charset);
	}

	/**
	 * 读取文件的每行内容到List<String>.
	 * 
	 * @see {@link Files#readAllLines}
	 */
	public static List<String> toLines(final File file) throws IOException {
		return Files.readAllLines(file.toPath(), Charsets.UTF_8);
	}

	/**
	 * 简单写入String到File.
	 */
	public static void write(final CharSequence data, final File file) throws IOException {
		write(data, file, Charsets.UTF_8);
	}
	/**
	 * 简单写入String到File.
	 */
	public static void write(final CharSequence data, final File file, Charset charset) throws IOException {
		Validate.notNull(file);
		Validate.notNull(data);

		try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), charset)) {
			writer.append(data);
		}
	}

	/**
	 * 追加String到File.
	 */
	public static void append(final CharSequence data, final File file) throws IOException {
		Validate.notNull(file);
		Validate.notNull(data);

		try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), Charsets.UTF_8,
				StandardOpenOption.APPEND)) {
			writer.append(data);
		}
	}

	/**
	 * 打开文件为InputStream.
	 * 
	 * @see {@link Files#newInputStream}
	 */
	public static InputStream asInputStream(String fileName) throws IOException {
		return asInputStream(getPath(fileName));
	}

	/**
	 * 打开文件为InputStream.
	 * 
	 * @see {@link Files#newInputStream}
	 */
	public static InputStream asInputStream(File file) throws IOException {
		Validate.notNull(file, "file is null");
		return asInputStream(file.toPath());
	}

	/**
	 * 打开文件为InputStream.
	 * 
	 * @see {@link Files#newInputStream}
	 */
	public static InputStream asInputStream(Path path) throws IOException {
		Validate.notNull(path, "path is null");
		return Files.newInputStream(path);
	}

	/**
	 * 打开文件为OutputStream.
	 * 
	 * @see {@link Files#newOutputStream}
	 */
	public static OutputStream asOututStream(String fileName) throws IOException {
		return asOututStream(getPath(fileName));
	}

	/**
	 * 打开文件为OutputStream.
	 * 
	 * @see {@link Files#newOutputStream}
	 */
	public static OutputStream asOututStream(File file) throws IOException {
		Validate.notNull(file, "file is null");
		return asOututStream(file.toPath());
	}

	/**
	 * 打开文件为OutputStream.
	 * 
	 * @see {@link Files#newOutputStream}
	 */
	public static OutputStream asOututStream(Path path) throws IOException {
		Validate.notNull(path, "path is null");
		return Files.newOutputStream(path);
	}

	/**
	 * 获取File的BufferedReader.
	 * 
	 * @see {@link Files#newBufferedReader}
	 */
	public static BufferedReader asBufferedReader(String fileName) throws IOException {
		Validate.notBlank(fileName, "filename is blank");
		return asBufferedReader(getPath(fileName));
	}

	public static BufferedReader asBufferedReader(Path path) throws IOException {
		Validate.notNull(path, "path is null");
		return Files.newBufferedReader(path, Charsets.UTF_8);
	}

	/**
	 * 获取File的BufferedWriter.
	 * 
	 * @see {@link Files#newBufferedWriter}
	 */
	public static BufferedWriter asBufferedWriter(String fileName) throws IOException {
		Validate.notBlank(fileName, "filename is blank");
		return Files.newBufferedWriter(getPath(fileName), Charsets.UTF_8);
	}

	/**
	 * 获取File的BufferedWriter.
	 * 
	 * @see {@link Files#newBufferedWriter}
	 */
	public static BufferedWriter asBufferedWriter(Path path) throws IOException {
		Validate.notNull(path, "path is null");
		return Files.newBufferedWriter(path, Charsets.UTF_8);
	}

	///// 文件操作 /////

	/**
	 * 复制文件或目录, not following links.
	 * 
	 * @param from 如果为null，或者是不存在的文件或目录，抛出异常.
	 * @param to 如果为null，或者from是目录而to是已存在文件，或相反
	 */
	public static void copy(@NotNull File from, @NotNull File to) throws IOException {
		Validate.notNull(from);
		Validate.notNull(to);

		copy(from.toPath(), to.toPath());
	}

	/**
	 * 复制文件或目录, not following links.
	 * 
	 * @param from 如果为null，或者是不存在的文件或目录，抛出异常.
	 * @param to 如果为null，或者from是目录而to是已存在文件，或相反
	 */
	public static void copy(@NotNull Path from, @NotNull Path to) throws IOException {
		Validate.notNull(from);
		Validate.notNull(to);

		if (Files.isDirectory(from)) {
			copyDir(from, to);
		} else {
			copyFile(from, to);
		}
	}

	/**
	 * 文件复制.
	 * 
	 * @see {@link Files#copy}
	 * 
	 * @param from 如果为null，或文件不存在或者是目录，，抛出异常
	 * @param to 如果to为null，或文件存在但是一个目录，抛出异常
	 */
	public static void copyFile(@NotNull File from, @NotNull File to) throws IOException {
		Validate.notNull(from);
		Validate.notNull(to);
		copyFile(from.toPath(), to.toPath());
	}

	/**
	 * 文件复制. @see {@link Files#copy}
	 * 
	 * @param from 如果为null，或文件不存在或者是目录，，抛出异常
	 * @param to 如果to为null，或文件存在但是一个目录，抛出异常
	 */
	public static void copyFile(@NotNull Path from, @NotNull Path to) throws IOException {
		Validate.isTrue(Files.exists(from), "%s is not exist or not a file", from);
		Validate.notNull(to);
		Validate.isTrue(!FileUtil.isDirExists(to), "%s is exist but it is a dir", to);
		Files.copy(from, to);
	}

	/**
	 * 复制目录
	 */
	public static void copyDir(@NotNull File from, @NotNull File to) throws IOException {
		Validate.isTrue(isDirExists(from), "%s is not exist or not a dir", from);
		Validate.notNull(to);

		copyDir(from.toPath(), to.toPath());
	}

	/**
	 * 复制目录
	 */
	public static void copyDir(@NotNull Path from, @NotNull Path to) throws IOException {
		Validate.isTrue(isDirExists(from), "%s is not exist or not a dir", from);
		Validate.notNull(to);
		makesureDirExists(to);

		try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(from)) {
			for (Path path : dirStream) {
				copy(path, to.resolve(path.getFileName()));
			}
		}
	}

	/**
	 * 文件移动/重命名.
	 * 
	 * @see {@link Files#move}
	 */
	public static void moveFile(@NotNull File from, @NotNull File to) throws IOException {
		Validate.notNull(from);
		Validate.notNull(to);

		moveFile(from.toPath(), to.toPath());
	}

	/**
	 * 文件移动/重命名.
	 * 
	 * @see {@link Files#move}
	 */
	public static void moveFile(@NotNull Path from, @NotNull Path to) throws IOException {
		Validate.isTrue(isFileExists(from), "%s is not exist or not a file", from);
		Validate.notNull(to);
		Validate.isTrue(!isDirExists(to), "%s is  exist but it is a dir", to);

		Files.move(from, to);
	}

	/**
	 * 目录移动/重命名
	 */
	public static void moveDir(@NotNull File from, @NotNull File to) throws IOException {
		Validate.isTrue(isDirExists(from), "%s is not exist or not a dir", from);
		Validate.notNull(to);
		Validate.isTrue(!isFileExists(to), "%s is exist but it is a file", to);

		final boolean rename = from.renameTo(to);
		if (!rename) {
			if (to.getCanonicalPath().startsWith(from.getCanonicalPath() + File.separator)) {
				throw new IOException("Cannot move directory: " + from + " to a subdirectory of itself: " + to);
			}
			copyDir(from, to);
			deleteDir(from);
			if (from.exists()) {
				throw new IOException("Failed to delete original directory '" + from + "' after copy to '" + to + '\'');
			}
		}
	}

	/**
	 * 创建文件或更新时间戳.
	 * 
	 * @see {@link com.google.common.io.Files#touch}
	 */
	public static void touch(String filePath) throws IOException {
		touch(new File(filePath));
	}

	/**
	 * 创建文件或更新时间戳.
	 * 
	 * @see {@link com.google.common.io.Files#touch}
	 */
	public static void touch(File file) throws IOException {
		com.google.common.io.Files.touch(file);
	}

	/**
	 * 删除文件.
	 * 
	 * 如果文件不存在或者是目录，则不做修改
	 */
	public static void deleteFile(@Nullable File file) throws IOException {
		Validate.isTrue(isFileExists(file), "%s is not exist or not a file", file);
		deleteFile(file.toPath());
	}

	/**
	 * 删除文件.
	 * 
	 * 如果文件不存在或者是目录，则不做修改
	 */
	public static void deleteFile(@Nullable Path path) throws IOException {
		Validate.isTrue(isFileExists(path), "%s is not exist or not a file", path);

		Files.delete(path);
	}

	/**
	 * 删除目录及所有子目录/文件
	 * 
	 * @see {@link Files#walkFileTree}
	 */
	public static void deleteDir(Path dir) throws IOException {
		Validate.isTrue(isDirExists(dir), "%s is not exist or not a dir", dir);

		// 后序遍历，先删掉子目录中的文件/目录
		Files.walkFileTree(dir, deleteFileVisitor);
	}

	/**
	 * 删除目录及所有子目录/文件
	 */
	public static void deleteDir(File dir) throws IOException {
		Validate.isTrue(isDirExists(dir), "%s is not exist or not a dir", dir);
		deleteDir(dir.toPath());
	}

	/**
	 * 判断目录是否存在, from Jodd
	 */
	public static boolean isDirExists(String dirPath) {
		if (dirPath == null) {
			return false;
		}
		return isDirExists(getPath(dirPath));
	}

	public static boolean isDirExists(Path dirPath) {
		return dirPath != null && Files.exists(dirPath) && Files.isDirectory(dirPath);
	}

	/**
	 * 判断目录是否存在, from Jodd
	 */
	public static boolean isDirExists(File dir) {
		if (dir == null) {
			return false;
		}
		return isDirExists(dir.toPath());
	}

	/**
	 * 确保目录存在, 如不存在则创建
	 */
	public static void makesureDirExists(String dirPath) throws IOException {
		makesureDirExists(getPath(dirPath));
	}

	/**
	 * 确保目录存在, 如不存在则创建
	 */
	public static void makesureDirExists(File file) throws IOException {
		Validate.notNull(file);
		makesureDirExists(file.toPath());
	}

	/**
	 * 确保目录存在, 如不存在则创建.
	 * 
	 * @see {@link Files#createDirectories}
	 * 
	 */
	public static void makesureDirExists(Path dirPath) throws IOException {
		Validate.notNull(dirPath);
		Files.createDirectories(dirPath);
	}

	/**
	 * 确保父目录及其父目录直到根目录都已经创建.
	 *
	 */
	public static void makesureParentDirExists(File file) throws IOException {
		Validate.notNull(file);
		makesureDirExists(file.getParentFile());
	}

	/**
	 * 判断文件是否存在, from Jodd.
	 * 
	 * @see {@link Files#exists}
	 * @see {@link Files#isRegularFile}
	 */
	public static boolean isFileExists(String fileName) {
		if (fileName == null) {
			return false;
		}
		return isFileExists(getPath(fileName));
	}

	/**
	 * 判断文件是否存在, from Jodd.
	 * 
	 * @see {@link Files#exists}
	 * @see {@link Files#isRegularFile}
	 */
	public static boolean isFileExists(File file) {
		if (file == null) {
			return false;
		}
		return isFileExists(file.toPath());
	}

	/**
	 * 判断文件是否存在, from Jodd.
	 * 
	 * @see {@link Files#exists}
	 * @see {@link Files#isRegularFile}
	 */
	public static boolean isFileExists(Path path) {
		if (path == null) {
			return false;
		}
		return Files.exists(path) && Files.isRegularFile(path);
	}

	/**
	 * 在临时目录创建临时目录，命名为${毫秒级时间戳}-${同一毫秒内的随机数}.
	 *
	 * @see {@link Files#createTempDirectory}
	 */
	public static Path createTempDir() throws IOException {
		return Files.createTempDirectory(System.currentTimeMillis() + "-");
	}

	/**
	 * 在临时目录创建临时文件，命名为tmp-${random.nextLong()}.tmp
	 * 
	 * @see {@link Files#createTempFile}
	 */
	public static Path createTempFile() throws IOException {
		return Files.createTempFile("tmp-", ".tmp");
	}

	/**
	 * 在临时目录创建临时文件，命名为${prefix}${random.nextLong()}${suffix}
	 * 
	 * @see {@link Files#createTempFile}
	 */
	public static Path createTempFile(String prefix, String suffix) throws IOException {
		return Files.createTempFile(prefix, suffix);
	}

	private static Path getPath(String filePath) {
		return Paths.get(filePath);
	}

	/**
	 * 获取文件名(不包含路径)
	 */
	public static String getFileNameByPlatform(@NotNull String fullName) {
		Validate.notEmpty(fullName);
		int last = fullName.lastIndexOf(Platforms.FILE_PATH_SEPARATOR_CHAR);
		return fullName.substring(last + 1);
	}
	/**
	 * 获取文件名(不包含路径)
	 */
	public static String getFileName(@NotNull String fullName) {
		Validate.notEmpty(fullName);
		String str = fullName.replace("\\", "/");
		int last = str.lastIndexOf("/");
		return str.substring(last + 1);
	}

	/**
	 * 获取文件名的扩展名部分(不包含.)
	 * 
	 * @see {@link com.google.common.io.Files#getFileExtension}
	 */
	public static String getFileExtension(File file) {
		return com.google.common.io.Files.getFileExtension(file.getName());
	}

	/**
	 * 获取文件名的扩展名部分(不包含.)
	 * 
	 * @see {@link com.google.common.io.Files#getFileExtension}
	 */
	public static String getFileExtension(String fullName) {
		return com.google.common.io.Files.getFileExtension(fullName);
	}
	
	public static boolean checkFileExtension(String filePath, String checkExtension) {
		String extension = getFileExtension(filePath);
		return extension.equalsIgnoreCase(checkExtension);
	}
	
	// -- 根据文件全路径创建目录以及文件 -- //
	public static boolean createFile(String name) {
		if (name == null || name.length() < 1) {
			return false;
		}
		return doCreateFile(name, name.length());
	}
	private static boolean doCreateFile(String name, int index) {
		String name1 = name.substring(0, index);
		File file1 = new File(name1);
		boolean ret = false;
		if (file1.exists()) {
			return true;
		}
		int i;
		for (i = index - 1; i > 1; --i) {
			if (name.charAt(i) == '/' || name.charAt(i) == '\\') {
				break;
			}
		}
		if (i > 1) {
			ret = doCreateFile(name, i);
		}
		String name2 = name.substring(0, index);
		File file2 = new File(name2);
		if (!file2.exists()) {
			if (name.length() == index && name.indexOf('.') != -1) {
				try {
					ret = file2.createNewFile();
				} catch (IOException e) {
					throw new RuntimeException("创建文件[" + name + "]失败！", e);
				}
			} else {
				ret = file2.mkdir();
			}
		}
		return ret;
	}
	
//	/**
//	 * 创建File对象，自动识别相对或绝对路径，相对路径将自动从ClassPath下寻找
//	 *
//	 * @param path 文件路径
//	 * @return File
//	 */
//	public static File file(String path) {
//		if (null == path) {
//			return null;
//		}
//		return new File(getAbsolutePath(path));
//	}

	/**
	 * 创建File对象<br>
	 * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
	 *
	 * @param parent 父目录
	 * @param path   文件路径
	 * @return File
	 */
	public static File createFile(String parent, String path) {
		return createFile(new File(parent), path);
	}

	/**
	 * 创建File对象<br>
	 * 根据的路径构建文件，在Win下直接构建，在Linux下拆分路径单独构建
	 * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
	 *
	 * @param parent 父文件对象
	 * @param path   文件路径
	 * @return File
	 */
	public static File createFile(File parent, String path) {
		if (StringUtils.isBlank(path)) {
			throw new NullPointerException("File path is blank!");
		}
		return checkSlip(parent, buildFile(parent, path));
	}

	/**
	 * 通过多层目录参数创建文件<br>
	 * 此方法会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
	 *
	 * @param directory 父目录
	 * @param names     元素名（多层目录名），由外到内依次传入
	 * @return the file 文件
	 * @since 4.0.6
	 */
	public static File createFile(File directory, String... names) {
		Validate.notNull(directory, "directory must not be null");
		if (ArrayUtil.isEmpty(names)) {
			return directory;
		}

		File file = directory;
		for (String name : names) {
			if (null != name) {
				file = createFile(file, name);
			}
		}
		return file;
	}

//	/**
//	 * 通过多层目录创建文件
//	 * <p>
//	 * 元素名（多层目录名）
//	 *
//	 * @param names 多层文件的文件名，由外到内依次传入
//	 * @return the file 文件
//	 * @since 4.0.6
//	 */
//	public static File file(String... names) {
//		if (ArrayUtil.isEmpty(names)) {
//			return null;
//		}
//
//		File file = null;
//		for (String name : names) {
//			if (file == null) {
//				file = file(name);
//			} else {
//				file = file(file, name);
//			}
//		}
//		return file;
//	}

	/**
	 * 创建File对象
	 *
	 * @param uri 文件URI
	 * @return File
	 */
	public static File createFile(URI uri) {
		if (uri == null) {
			throw new NullPointerException("File uri is null!");
		}
		return new File(uri);
	}

	/**
	 * 创建File对象
	 *
	 * @param url 文件URL
	 * @return File
	 */
	public static File createFile(URL url) {
		return new File(UrlUtil.toURI(url));
	}
	
	public static void appendFileToFile(File src, File dest) {
		InputStream fin = null;
		OutputStream fout = null;
		try {
			if (dest.exists()) {
				fout = new BufferedOutputStream(new FileOutputStream(dest, true), BUFFER_SIZE); // 追加
			} else {
				fout = new BufferedOutputStream(new FileOutputStream(dest), BUFFER_SIZE); // 新增
			}
			fin = new BufferedInputStream(new FileInputStream(src), BUFFER_SIZE);
			byte[] buffer = new byte[BUFFER_SIZE];
			int len = 0;
			while ((len = fin.read(buffer)) > 0) {
				fout.write(buffer, 0, len);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (fin != null) {
				try {
					fin.close();
				} catch (IOException e) {
				}
			}
			if (fout != null) {
				try {
					fout.close();
				} catch (IOException e) {
				}
			}
		}
	}
	
	/**
     * 获取文件类型
     * 
     * @param photoByte 文件字节码
     * @return 后缀（不含".")
     */
    public static String getFileExtension(byte[] photoByte)
    {
        String strFileExtendName = "JPG";
        if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56)
                && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97))
        {
            strFileExtendName = "GIF";
        }
        else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70))
        {
            strFileExtendName = "JPG";
        }
        else if ((photoByte[0] == 66) && (photoByte[1] == 77))
        {
            strFileExtendName = "BMP";
        }
        else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71))
        {
            strFileExtendName = "PNG";
        }
        return strFileExtendName;
    }
	
    /**
	 * 判断文件是否存在，如果file为null，则返回false
	 *
	 * @param file 文件
	 * @return 如果存在返回true
	 */
	public static boolean exist(File file) {
		return (null != file) && file.exists();
	}

	/**
	 * 是否存在匹配文件
	 *
	 * @param directory 文件夹路径
	 * @param regexp    文件夹中所包含文件名的正则表达式
	 * @return 如果存在匹配文件返回true
	 */
	public static boolean exist(String directory, String regexp) {
		final File file = new File(directory);
		if (false == file.exists()) {
			return false;
		}

		final String[] fileList = file.list();
		if (fileList == null) {
			return false;
		}

		for (String fileName : fileList) {
			if (fileName.matches(regexp)) {
				return true;
			}

		}
		return false;
	}

	/**
	 * 指定文件最后修改时间
	 *
	 * @param file 文件
	 * @return 最后修改时间
	 */
	public static Date lastModifiedTime(File file) {
		if (false == exist(file)) {
			return null;
		}

		return new Date(file.lastModified());
	}

	/**
	 * 指定路径文件最后修改时间
	 *
	 * @param path 绝对路径
	 * @return 最后修改时间
	 */
	public static Date lastModifiedTime(String path) {
		return lastModifiedTime(new File(path));
	}

	/**
	 * 计算目录或文件的总大小<br>
	 * 当给定对象为文件时，直接调用 {@link File#length()}<br>
	 * 当给定对象为目录时，遍历目录下的所有文件和目录，递归计算其大小，求和返回
	 *
	 * @param file 目录或文件,null或者文件不存在返回0
	 * @return 总大小，bytes长度
	 */
	public static long size(File file) {
		if (null == file || false == file.exists() || isSymlink(file)) {
			return 0;
		}

		if (file.isDirectory()) {
			long size = 0L;
			File[] subFiles = file.listFiles();
			if (ArrayUtil.isEmpty(subFiles)) {
				return 0L;// empty directory
			}
			for (File subFile : subFiles) {
				size += size(subFile);
			}
			return size;
		} else {
			return file.length();
		}
	}
	
	/**
	 * 判断是否为符号链接文件
	 *
	 * @param file 被检查的文件
	 * @return 是否为符号链接文件
	 * @since 4.4.2
	 */
	public static boolean isSymlink(File file) {
		return isSymlink(file.toPath());
	}
	
	/**
	 * 判断是否为符号链接文件
	 *
	 * @param path 被检查的文件
	 * @return 是否为符号链接文件
	 * @since 4.4.2
	 */
	public static boolean isSymlink(Path path) {
		return Files.isSymbolicLink(path);
	}
	
	/**
	 * 获取标准的绝对路径
	 *
	 * @param file 文件
	 * @return 绝对路径
	 */
	public static String getAbsolutePath(File file) {
		if (file == null) {
			return null;
		}

		try {
			return file.getCanonicalPath();
		} catch (IOException e) {
			return file.getAbsolutePath();
		}
	}
	
	/**
	 * 根据压缩包中的路径构建目录结构，在Win下直接构建，在Linux下拆分路径单独构建
	 *
	 * @param outFile  最外部路径
	 * @param fileName 文件名，可以包含路径
	 * @return 文件或目录
	 * @since 5.0.5
	 */
	private static File buildFile(File outFile, String fileName) {
		// 替换Windows路径分隔符为Linux路径分隔符，便于统一处理
		fileName = fileName.replace('\\', '/');
		if (false == Platforms.IS_WINDOWS
				// 检查文件名中是否包含"/"，不考虑以"/"结尾的情况
				&& fileName.lastIndexOf(CharPool.SLASH, fileName.length() - 2) > 0) {
			// 在Linux下多层目录创建存在问题，/会被当成文件名的一部分，此处做处理
			// 使用/拆分路径（zip中无\），级联创建父目录
			final List<String> pathParts = MoreStringUtil.split(fileName, '/');
			final int lastPartIndex = pathParts.size() - 1;//目录个数
			for (int i = 0; i < lastPartIndex; i++) {
				//由于路径拆分，slip不检查，在最后一步检查
				outFile = new File(outFile, pathParts.get(i));
			}
			//noinspection ResultOfMethodCallIgnored
			outFile.mkdirs();
			// 最后一个部分如果非空，作为文件名
			fileName = pathParts.get(lastPartIndex);
		}
		return new File(outFile, fileName);
	}
	
	/**
	 * 检查父完整路径是否为自路径的前半部分，如果不是说明不是子路径，可能存在slip注入。
	 * <p>
	 * 见http://blog.nsfocus.net/zip-slip-2/
	 *
	 * @param parentFile 父文件或目录
	 * @param file       子文件或目录
	 * @return 子文件或目录
	 * @throws IllegalArgumentException 检查创建的子文件不在父目录中抛出此异常
	 */
	public static File checkSlip(File parentFile, File file) throws IllegalArgumentException {
		if (null != parentFile && null != file) {
			String parentCanonicalPath;
			String canonicalPath;
			try {
				parentCanonicalPath = parentFile.getCanonicalPath();
				canonicalPath = file.getCanonicalPath();
			} catch (IOException e) {
				// issue#I4CWMO@Gitee
				// getCanonicalPath有时会抛出奇怪的IO异常，此时忽略异常，使用AbsolutePath判断。
				parentCanonicalPath = parentFile.getAbsolutePath();
				canonicalPath = file.getAbsolutePath();
			}
			if (false == canonicalPath.startsWith(parentCanonicalPath)) {
				throw new IllegalArgumentException("New file is outside of the parent dir: " + file.getName());
			}
		}
		return file;
	}
	
	/**
	 * 修复路径<br>
	 * 如果原路径尾部有分隔符，则保留为标准分隔符（/），否则不保留
	 * <ol>
	 * <li>1. 统一用 /</li>
	 * <li>2. 多个 / 转换为一个 /</li>
	 * <li>3. 去除左边空格</li>
	 * <li>4. .. 和 . 转换为绝对路径，当..多于已有路径时，直接返回根路径</li>
	 * </ol>
	 * <p>
	 * 栗子：
	 *
	 * <pre>
	 * "/foo//" =》 "/foo"
	 * "/foo/./" =》 "/foo"
	 * "/foo/../bar" =》 "/bar"
	 * "/foo/../bar/" =》 "/bar"
	 * "/foo/../bar/../baz" =》 "/baz"
	 * "/../" =》 "/"
	 * "foo/bar/.." =》 "foo"
	 * "foo/../bar" =》 "bar"
	 * "foo/../../bar" =》 "../bar"
	 * "//server/foo/../bar" =》 "/server/bar"
	 * "//server/../bar" =》 "/bar"
	 * "C:\\foo\\..\\bar" =》 "C:/bar"
	 * "C:\\..\\bar" =》 "C:/bar"
	 * "~/foo/../bar/" =》 "~/bar"
	 * "~/../bar" =》 普通用户运行是'bar的home目录'，ROOT用户运行是'/bar'
	 * </pre>
	 *
	 * @param path 原路径
	 * @return 修复后的路径
	 */
	public static String normalize(String path) {
		if (path == null) {
			return null;
		}

		// 兼容Spring风格的ClassPath路径，去除前缀，不区分大小写
		String pathToUse = StringUtils.removeStartIgnoreCase(path, UrlUtil.CLASSPATH_URL_PREFIX);
		// 去除file:前缀
		pathToUse = StringUtils.removeStartIgnoreCase(pathToUse, UrlUtil.FILE_URL_PREFIX);

		// 识别home目录形式，并转换为绝对路径
		if (StringUtils.startsWith(pathToUse, "~")) {
			pathToUse = Platforms.USER_HOME + pathToUse.substring(1);
		}

		// 统一使用斜杠
		pathToUse = pathToUse.replaceAll("[/\\\\]+", StrPool.SLASH);
		// 去除开头空白符，末尾空白符合法，不去除
		pathToUse = MoreStringUtil.trimStart(pathToUse);
		//兼容Windows下的共享目录路径（原始路径如果以\\开头，则保留这种路径）
		if (path.startsWith("\\\\")) {
			pathToUse = "\\" + pathToUse;
		}

		String prefix = StringUtils.EMPTY;
		int prefixIndex = pathToUse.indexOf(StrPool.COLON);
		if (prefixIndex > -1) {
			// 可能Windows风格路径
			prefix = pathToUse.substring(0, prefixIndex + 1);
			if (StringUtils.startsWith(prefix, StrPool.SLASH)) {
				// 去除类似于/C:这类路径开头的斜杠
				prefix = prefix.substring(1);
			}
			if (false == prefix.contains(StrPool.SLASH)) {
				pathToUse = pathToUse.substring(prefixIndex + 1);
			} else {
				// 如果前缀中包含/,说明非Windows风格path
				prefix = StringUtils.EMPTY;
			}
		}
		if (pathToUse.startsWith(StrPool.SLASH)) {
			prefix += StrPool.SLASH;
			pathToUse = pathToUse.substring(1);
		}

		String[] pathArr = StringUtils.split(pathToUse, StrPool.C_SLASH);

		List<String> pathElements = new LinkedList<>();
		int tops = 0;
		String element;
		for (int i = pathArr.length - 1; i >= 0; i--) {
			element = pathArr[i];
			// 只处理非.的目录，即只处理非当前目录
			if (false == StrPool.DOT.equals(element)) {
				if (StrPool.DOUBLE_DOT.equals(element)) {
					tops++;
				} else {
					if (tops > 0) {
						// 有上级目录标记时按照个数依次跳过
						tops--;
					} else {
						// Normal path element found.
						pathElements.add(0, element);
					}
				}
			}
		}

		// issue#1703@Github
		if (tops > 0 && StringUtils.isEmpty(prefix)) {
			// 只有相对路径补充开头的..，绝对路径直接忽略之
			while (tops-- > 0) {
				//遍历完节点发现还有上级标注（即开头有一个或多个..），补充之
				// Normal path element found.
				pathElements.add(0, StrPool.DOUBLE_DOT);
			}
		}

		return prefix + StringUtils.join(pathElements, StrPool.SLASH);
	}
	
	/**
	 * 获取父目录路径
	 */
	public static String normalizeParentPath(String path) {
		String parentPath = normalize(path);
		if (parentPath == null) {
			return parentPath;
		}
		int index = parentPath.lastIndexOf("/");
		return (index <= 0) ? "/" : parentPath.substring(0, index);
	}
	
	/**
	 * 覆盖方式移动文件
	 */
	public static void overrideMoveFile(File from, File to) throws Exception {
		Validate.notNull(from);
		Validate.notNull(to);
		try {
			if (to.exists() && to.isFile()) {
				to.delete();
				log.info("文件{}已被清除", to.getName());
			}
		} catch(Exception e) {
			log.error("清理文件" + to.getName() + "异常", e);
		}
		moveFile(from, to);
	}
	/**
	 * 覆盖方式修改文件名
	 */
	public static void overrideRenameTo(File from, File to) {
		Validate.notNull(from);
		Validate.notNull(to);
		try {
			if (to.exists() && to.isFile()) {
				to.delete();
				log.info("文件{}已被清除", to.getName());
			}
		} catch(Exception e) {
			log.error("清理文件" + to.getName() + "异常", e);
		}
		boolean ret = from.renameTo(to);
		if (ret) {
			log.info("修改文件名{}->{}成功", from.getAbsolutePath(), to.getAbsolutePath());
		} else {
			log.error("修改文件名{}->{}失败", from.getAbsolutePath(), to.getAbsolutePath());
		}
	}
	/**
	 * 覆盖方式移动文件（移动使用临时文件，移动完成后自动改名）
	 * 比如：overrideMoveFileExt("upload/aaa.txt", "/data/aaa.txt", "tmp")
	 * 执行过程：
	 * 	1 移动upload/aaa.txt到/data/aaa.txt.tmp（覆盖方式）
	 * 	2 /data/aaa.txt.tmp重命名为/data/aaa.txt（覆盖方式）
	 */
	public static void overrideMoveFileExt(String fromFile, String toFile, String extName) throws Exception {
		Validate.notBlank(fromFile);
		Validate.notBlank(toFile);
		Validate.notBlank(extName);
		File toTmpFile = new File(toFile + "." + extName);
		// 移动临时文件
		overrideMoveFile(new File(fromFile), toTmpFile);
		// 临时文件改名
		overrideRenameTo(toTmpFile, new File(toFile));
	}
	/**
	 * 清理文件
	 */
	public static void clearFile(File file) {
		if (file == null) {
			return ;
		}
		try {
			if (file.exists() && file.isFile()) {
				boolean ret = file.delete();
				if (ret) log.info("删除文件{}成功", file.getName());
				else log.error("删除文件{}失败", file.getName());
			}
		} catch(Exception e) {
			log.error("清理文件" + file.getName() + "异常", e);
		}
	}
	
}
